package com.example.service;

import com.example.exception.FileUrlParseException;
import com.example.exception.InvalidFileTypeException;
import com.example.exception.StorageException;
import com.example.util.StringUtil;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.PatternSyntaxException;

/**
 * 服务器上传图片文件服务接口, 这个接口的默认实现是线程不安全的
 * 在多线程情况下, 检查目录是否存在(适用于第一次创建目录时), 部分线程将会失败
 * @author LeXin Huang
 * @date 2021年03月25日 14:29
 */
public interface ServerUploadFileService {

    /**
     * 获取文件存储的路径分隔符, 可能的取值是 '/', '\'
     *
     * @return 路径分隔符
     */
    String getDirectoryPathSeparator();

    /**
     * 获取文件资源 url 的前缀, 一般以 http:// 开头 (不包含文件名称)
     *
     * @return 文件资源超链接前缀
     */
    String getFileUrlPrefix();

    /**
     * 获取文件在持久性存储上的目录路径
     *
     * @return 文件在持久性存储上的目录路径
     */
    String getStorageDirectoryPath();

    /**
     * 保存上传的文件
     *
     * @param files 要上传的文件
     * @return 创建的文件的超链接
     * @throws InvalidFileTypeException 当上传的文件格式有误时
     * @throws StorageException     当创建文件失败或创建目录失败时
     */
    default List<String> upload(MultipartFile[] files) {
        if (null == files || 0 == files.length) {
            return Collections.emptyList();
        }
        List<String> fileNameSuffixes = new ArrayList<>();
        List<String> urls = new ArrayList<>();
        for (MultipartFile file : files) {
            String fileNameSuffix = checkFileType(file);
            fileNameSuffixes.add(fileNameSuffix);
        }
        try {
            mkDirsIfNotExists();
            for (int i = 0; i < files.length; i++) {
                String fileName = storage(files[i], fileNameSuffixes.get(i));
                String fileUrl = getFileUrlPrefix() + fileName;
                urls.add(fileUrl);
            }
        } catch (IOException e) {
            handleError(urls, e);
        }
        return urls;
    }

    /**
     * 尝试将上传失败过程中的已上传文件逐一删除
     * @param uploadedFilesUrl 已经上传成功文件的url
     * @param e 产生的异常
     */
    default void handleError(List<String> uploadedFilesUrl, IOException e) {
        for (String fileUrl : uploadedFilesUrl) {
            try {
                deleteFileByUrl(fileUrl);
            } catch (Exception ignored) {}
        }
        throw new StorageException(e.getMessage());
    }

    /**
     * 保存要上传的文件
     * @param file 要保存的文件
     * @param fileNameSuffix 文件后缀名
     * @return 访问文件资源的url
     * @throws IOException 当创建文件失败时
     */
    private String storage(MultipartFile file, String fileNameSuffix) throws IOException {
        String fileName = generateUniqueFileName(fileNameSuffix);
        File target = new File(getFilePath(fileName));
        if (target.exists()) {
            throw new StorageException("原名称的文件仍然存在! 创建失败!");
        }
        boolean createSuccess = target.createNewFile();
        if (!createSuccess) {
            throw new StorageException("创建文件失败!");
        }
        file.transferTo(target);
        return fileName;
    }

    /**
     * 为创建文件前进行必要的目录检查, 如果目录不存在, 将会创建一个新的目录
     * 注意: 当已存在同名的文件(而不是目录)时, 将不会覆盖该文件, 而是会抛出异常, 以避免文件被错误覆盖
     * 这个方法实现是线程不安全的, 可能会出现多个线程同时创建目录, 而导致未知的错误
     * @throws StorageException 创建文件夹失败时, 或者同名的文件已经存在
     */
    private void mkDirsIfNotExists() {
        File dir = new File(getStorageDirectoryPath());
        if (!dir.exists()) {
            boolean makeSuccess = dir.mkdirs();
            if (!makeSuccess) {
                throw new StorageException("创建文件夹失败!");
            }
        } else if (!dir.isDirectory()) {
            throw new StorageException("同名的文件已经存在, 创建文件夹失败!");
        }
    }

    /**
     * 检查上传文件的类型是否支持
     *
     * @param file 要上传的文件
     * @return 支持的文件名后缀
     * @throws InvalidFileTypeException 当文件不符合所需类型时
     */
    default String checkFileType(MultipartFile file) {
        if (Objects.isNull(file)) {
            throw new InvalidFileTypeException("不能上传空文件!");
        }
        if (Objects.isNull(file.getOriginalFilename())) {
            throw new InvalidFileTypeException("所选文件的名称不能为空!");
        }
        String imageFileName = file.getOriginalFilename();
        String[] supportedTypes = Objects.requireNonNullElse(getSupportedFileTypeNames(), new String[0]);
        imageFileName = imageFileName.toLowerCase(Locale.ROOT);
        for (String supportedType : supportedTypes) {
            if (imageFileName.endsWith(supportedType)) {
                return supportedType;
            }
        }
        throw new InvalidFileTypeException("所选文件中包含不支持的类型的文件, 仅支持类型为 " + Arrays.toString(supportedTypes) + "的文件!");
    }

    /**
     * 获取文件类型名称数组, 一般以 "." 字符开头, 如 ".jpg", ".doc" 等
     *
     * @return 文件类型名称数组
     */
    String[] getSupportedFileTypeNames();


    /**
     * 生成文件名称 (确保名称唯一)
     *
     * @param suffix 文件名后缀 如 {.jpg, .png}
     * @return 文件名称
     */
    private String generateUniqueFileName(String suffix) {
        return StringUtil.getUniqueCharSequence() + suffix;
    }

    /**
     * 根据文件 url 删除指定文件
     *
     * @param fileUrl 文件 url
     * @return true 当文件不存在或者删除操作成功时, 否则 false
     * @throws FileUrlParseException 当文件资源 url 无法正确解析出文件名称时
     */
    default boolean deleteFileByUrl(String fileUrl) {
        String fileName = parseFileName(fileUrl);
        String filePath = getFilePath(fileName);
        File fileToDelete = new File(filePath);
        boolean deleteSuccess = fileToDelete.delete();
        return !fileToDelete.exists() || deleteSuccess;
    }

    /**
     * 根据 url 解析出文件名称
     *
     * @param fileUrl 文件url
     * @return 文件名称(如果格式正确)
     */
    private String parseFileName(String fileUrl) {
        try {
            return fileUrl.split(getFileUrlPrefix())[1];
        } catch (PatternSyntaxException e) {
            throw new FileUrlParseException("文件资源url: " + fileUrl + ";无法解析到指定文件!");
        }
    }

    /**
     * 通过文件名, 获取文件路径
     *
     * @param fileName 文件名称
     * @return 文件存储路径
     */
    private String getFilePath(String fileName) {
        return getStorageDirectoryPath() + getDirectoryPathSeparator() + fileName;
    }


}
